/* QtLua -- Lua bindings for Qt
   Copyright (C) 2011, Jarek Pelczar

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General
   Public License along with this library; if not, write to the
   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301 USA

*/

#include "qluaqobjectwrapper_p.h"
#include "qluascriptengine_p.h"
#include "qluaengineglobals_p.h"
#include "qluascriptcontext_p.h"
#include "qluascriptcontextinfo_p.h"
#include "qluascriptclass.h"
#include <QDebug>
#include <QMetaProperty>

namespace QLuaInternal {

#define QLuaMetaPropertyWrapper_WrapperName "__QtObject_qtlua_metatable_QLuaMetaPropertyWrapper"
#define QLuaMetaMethodWrapper_WrapperName "__QtObject_qtlua_metatable_QLuaMetaMethodWrapper"

static inline bool methodNameEquals(const QMetaMethod &method,
                                    const char *signature, int nameLength)
{
    const char *otherSignature = method.signature();
    return !qstrncmp(otherSignature, signature, nameLength)
            && (otherSignature[nameLength] == '(');
}

class QScriptMetaType
{
public:
    enum Kind {
        Invalid,
        Variant,
        MetaType,
        Unresolved,
        MetaEnum
    };

    inline QScriptMetaType()
        : m_kind(Invalid) { }

    inline Kind kind() const
    { return m_kind; }

    int typeId() const;

    inline bool isValid() const
    { return (m_kind != Invalid); }

    inline bool isVariant() const
    { return (m_kind == Variant); }

    inline bool isMetaType() const
    { return (m_kind == MetaType); }

    inline bool isUnresolved() const
    { return (m_kind == Unresolved); }

    inline bool isMetaEnum() const
    { return (m_kind == MetaEnum); }

    QByteArray name() const;

    inline int enumeratorIndex() const
    { Q_ASSERT(isMetaEnum()); return m_typeId; }

    inline bool operator==(const QScriptMetaType &other) const
    {
        return (m_kind == other.m_kind) && (m_typeId == other.m_typeId);
    }

    static inline QScriptMetaType variant()
    { return QScriptMetaType(Variant); }

    static inline QScriptMetaType metaType(int typeId, const QByteArray &name)
    { return QScriptMetaType(MetaType, typeId, name); }

    static inline QScriptMetaType metaEnum(int enumIndex, const QByteArray &name)
    { return QScriptMetaType(MetaEnum, enumIndex, name); }

    static inline QScriptMetaType unresolved(const QByteArray &name)
    { return QScriptMetaType(Unresolved, /*typeId=*/0, name); }

private:
    inline QScriptMetaType(Kind kind, int typeId = 0, const QByteArray &name = QByteArray())
        : m_kind(kind), m_typeId(typeId), m_name(name) { }

    Kind m_kind;
    int m_typeId;
    QByteArray m_name;
};



int QScriptMetaType::typeId() const
{
    if (isVariant())
        return QMetaType::type("QVariant");
    return isMetaEnum() ? 2/*int*/ : m_typeId;
}

QByteArray QScriptMetaType::name() const
{
    if (!m_name.isEmpty())
        return m_name;
    else if (m_kind == Variant)
        return "QVariant";
    return QMetaType::typeName(typeId());
}

class QScriptMetaMethod
{
public:
    inline QScriptMetaMethod()
    { }
    inline QScriptMetaMethod(const QVector<QScriptMetaType> &types)
        : m_types(types), m_firstUnresolvedIndex(-1)
    {
        QVector<QScriptMetaType>::const_iterator it;
        for (it = m_types.constBegin(); it != m_types.constEnd(); ++it) {
            if ((*it).kind() == QScriptMetaType::Unresolved) {
                m_firstUnresolvedIndex = it - m_types.constBegin();
                break;
            }
        }
    }
    inline bool isValid() const
    { return !m_types.isEmpty(); }

    inline QScriptMetaType returnType() const
    { return m_types.at(0); }

    inline int argumentCount() const
    { return m_types.count() - 1; }

    inline QScriptMetaType argumentType(int arg) const
    { return m_types.at(arg + 1); }

    inline bool fullyResolved() const
    { return m_firstUnresolvedIndex == -1; }

    inline bool hasUnresolvedReturnType() const
    { return (m_firstUnresolvedIndex == 0); }

    inline int firstUnresolvedIndex() const
    { return m_firstUnresolvedIndex; }

    inline int count() const
    { return m_types.count(); }

    inline QScriptMetaType type(int index) const
    { return m_types.at(index); }

    inline QVector<QScriptMetaType> types() const
    { return m_types; }

private:
    QVector<QScriptMetaType> m_types;
    int m_firstUnresolvedIndex;
};

struct QScriptMetaArguments
{
    int matchDistance;
    int index;
    QScriptMetaMethod method;
#ifdef QT_DEBUG
    QVector<QVariant> args;
#else
    QVarLengthArray<QVariant, 9> args;
#endif

#ifdef QT_DEBUG
    inline QScriptMetaArguments(int dist, int idx, const QScriptMetaMethod &mtd,
                                const QVector<QVariant> &as)
#else
    inline QScriptMetaArguments(int dist, int idx, const QScriptMetaMethod &mtd,
                                const QVarLengthArray<QVariant, 9> &as)
#endif

        : matchDistance(dist), index(idx), method(mtd), args(as) { }
    inline QScriptMetaArguments()
        : index(-1) { }

    inline bool isValid() const
    { return (index != -1); }
};

static QMetaMethod metaMethod(const QMetaObject *meta,
                              QMetaMethod::MethodType type,
                              int index)
{
    if (type != QMetaMethod::Constructor)
        return meta->method(index);
    else
        return meta->constructor(index);
}

static bool convertToNativeQObject(QLuaScriptEnginePrivate *exec, QLuaScriptValue value,
                                   const QByteArray &targetType,
                                   void **result)
{
    if (!targetType.endsWith('*'))
        return false;
    if (QObject *qobject = value.toQObject()) {
        int start = targetType.startsWith("const ") ? 6 : 0;
        QByteArray className = targetType.mid(start, targetType.size()-start-1);
        if (void *instance = qobject->qt_metacast(className)) {
            *result = instance;
            return true;
        }
    }
    return false;
}

static inline int methodNameLength(const QMetaMethod &method)
{
    const char *signature = method.signature();
    const char *s = signature;
    while (*s && (*s != '('))
        ++s;
    return s - signature;
}

static inline QByteArray methodName(const char *signature, int nameLength)
{
    return QByteArray(signature, nameLength);
}

static int indexOfMetaEnum(const QMetaObject *meta, const QByteArray &str)
{
    QByteArray scope;
    QByteArray name;
    int scopeIdx = str.lastIndexOf("::");
    if (scopeIdx != -1) {
        scope = str.left(scopeIdx);
        name = str.mid(scopeIdx + 2);
    } else {
        name = str;
    }
    for (int i = meta->enumeratorCount() - 1; i >= 0; --i) {
        QMetaEnum m = meta->enumerator(i);
        if ((m.name() == name) && (scope.isEmpty() || (m.scope() == scope)))
            return i;
    }
    return -1;
}

static QLuaScriptValue callQtMethod(QLuaScriptEnginePrivate * exec,
                                    QMetaMethod::MethodType callType,
                                    QObject *thisQObject,
                                    QVector<QLuaScriptValue> scriptArgs,
                                    const QLuaScriptValue& thisObject,
                                    const QMetaObject * meta,
                                    int initialIndex,
                                    bool maybeOverloaded)
{
    QScriptMetaMethod chosenMethod;
    int chosenIndex = -1;
#ifdef QT_DEBUG
    QVector<QVariant> args;
#else
    QVarLengthArray<QVariant, 9> args;
#endif
    QVector<QScriptMetaArguments> candidates;
    QVector<QScriptMetaArguments> unresolved;
    QVector<int> tooFewArgs;
    QVector<int> conversionFailed;
    int index;
    int nameLength = 0;
    const char *initialMethodSignature = 0;

    for (index = initialIndex; index >= 0; --index) {
        QMetaMethod method = metaMethod(meta, callType, index);

        if (index == initialIndex) {
            initialMethodSignature = method.signature();
            nameLength = methodNameLength(method);
        } else {
            if (!methodNameEquals(method, initialMethodSignature, nameLength))
                continue;
        }

        QList<QByteArray> parameterTypeNames = method.parameterTypes();

        QVector<QScriptMetaType> types;
        types.resize(1 + parameterTypeNames.size());
        QScriptMetaType *typesData = types.data();
        // resolve return type
        QByteArray returnTypeName = method.typeName();
        int rtype = QMetaType::type(returnTypeName);
        if ((rtype == 0) && !returnTypeName.isEmpty()) {
            int enumIndex = indexOfMetaEnum(meta, returnTypeName);
            if (enumIndex != -1)
                typesData[0] = QScriptMetaType::metaEnum(enumIndex, returnTypeName);
            else
                typesData[0] = QScriptMetaType::unresolved(returnTypeName);
        } else {
            if (callType == QMetaMethod::Constructor)
                typesData[0] = QScriptMetaType::metaType(QMetaType::QObjectStar, "QObject*");
            else if (rtype == QMetaType::QVariant)
                typesData[0] = QScriptMetaType::variant();
            else
                typesData[0] = QScriptMetaType::metaType(rtype, returnTypeName);
        }

        // resolve argument types
        for (int i = 0; i < parameterTypeNames.count(); ++i) {
            QByteArray argTypeName = parameterTypeNames.at(i);
            int atype = QMetaType::type(argTypeName);
            if (atype == 0) {
                int enumIndex = indexOfMetaEnum(meta, argTypeName);
                if (enumIndex != -1)
                    typesData[1 + i] = QScriptMetaType::metaEnum(enumIndex, argTypeName);
                else
                    typesData[1 + i] = QScriptMetaType::unresolved(argTypeName);
            } else if (atype == QMetaType::QVariant) {
                typesData[1 + i] = QScriptMetaType::variant();
            } else {
                typesData[1 + i] = QScriptMetaType::metaType(atype, argTypeName);
            }
        }

        QScriptMetaMethod mtd = QScriptMetaMethod(types);

        if (int(scriptArgs.size()) < mtd.argumentCount()) {
            tooFewArgs.append(index);
            continue;
        }

        if (!mtd.fullyResolved()) {
            // remember it so we can give an error message later, if necessary
#ifdef QT_DEBUG
            unresolved.append(QScriptMetaArguments(/*matchDistance=*/INT_MAX, index,
                                                   mtd, QVector<QVariant>()));
#else
            unresolved.append(QScriptMetaArguments(/*matchDistance=*/INT_MAX, index,
                                                   mtd, QVarLengthArray<QVariant, 9>()));
#endif
            if (mtd.hasUnresolvedReturnType())
                continue;
        }

        if (args.count() != mtd.count())
            args.resize(mtd.count());

        QScriptMetaType retType = mtd.returnType();
        args[0] = QVariant(retType.typeId(), (void *)0); // the result

        // try to convert arguments
        bool converted = true;
        int matchDistance = 0;
        for (int i = 0; converted && i < mtd.argumentCount(); ++i) {
            QLuaScriptValue actual;

            if (i < (int)scriptArgs.size())
                actual = scriptArgs.at(i);
            else
                actual = QLuaScriptValue(QLuaScriptValue::NilValue);

            QScriptMetaType argType = mtd.argumentType(i);
            int tid = -1;
            QVariant v;
            if (argType.isUnresolved()) {
                v = QVariant(QMetaType::QObjectStar, (void *)0);
                converted = convertToNativeQObject(
                            exec, actual, argType.name(), reinterpret_cast<void* *>(v.data()));
            } else if (argType.isVariant()) {
                //                if (QScriptEnginePrivate::isVariant(actual)) {
                //                    v = QScriptEnginePrivate::variantValue(actual);
                //                } else {
                v = actual.toVariant();
                converted = v.isValid() || actual.isNil();
                //                }
            } else {
                tid = argType.typeId();
                v = QVariant(tid, (void *)0);

                converted = exec->convertScriptValueToPropertyType(argType.isMetaEnum(), tid, actual, v);
            }

            if (!converted) {
                if (tid == -1)
                    tid = argType.typeId();
                QVariant vv = actual.toVariant();
                if (vv.canConvert(QVariant::Type(tid))) {
                    v = vv;
                    converted = v.convert(QVariant::Type(tid));
                    if (converted && (vv.userType() != tid))
                        matchDistance += 10;
                } else {
                    QByteArray vvTypeName = vv.typeName();
                    if (vvTypeName.endsWith('*')
                            && (vvTypeName.left(vvTypeName.size()-1) == argType.name())) {
                        v = QVariant(tid, *reinterpret_cast<void* *>(vv.data()));
                        converted = true;
                        matchDistance += 10;
                    }
                }
            } else {
                // determine how well the conversion matched
                if (actual.isNumber()) {
                    switch (tid) {
                    case QMetaType::Double:
                        // perfect
                        break;
                    case QMetaType::Float:
                        matchDistance += 1;
                        break;
                    case QMetaType::LongLong:
                    case QMetaType::ULongLong:
                        matchDistance += 2;
                        break;
                    case QMetaType::Long:
                    case QMetaType::ULong:
                        matchDistance += 3;
                        break;
                    case QMetaType::Int:
                    case QMetaType::UInt:
                        matchDistance += 4;
                        break;
                    case QMetaType::Short:
                    case QMetaType::UShort:
                        matchDistance += 5;
                        break;
                    case QMetaType::Char:
                    case QMetaType::UChar:
                        matchDistance += 6;
                        break;
                    default:
                        matchDistance += 10;
                        break;
                    }
                } else if (actual.isString()) {
                    switch (tid) {
                    case QMetaType::QString:
                        // perfect
                        break;
                    default:
                        matchDistance += 10;
                        break;
                    }
                } else if (actual.isBool()) {
                    switch (tid) {
                    case QMetaType::Bool:
                        // perfect
                        break;
                    default:
                        matchDistance += 10;
                        break;
                    }
                } else if (actual.isQObject()) {
                    switch (tid) {
                    case QMetaType::QObjectStar:
                    case QMetaType::QWidgetStar:
                        // perfect
                        break;
                    default:
                        matchDistance += 10;
                        break;
                    }
                } else if (actual.isNil()) {
                    switch (tid) {
                    case QMetaType::VoidStar:
                    case QMetaType::QObjectStar:
                    case QMetaType::QWidgetStar:
                        // perfect
                        break;
                    default:
                        if (!argType.name().endsWith('*'))
                            matchDistance += 10;
                        break;
                    }
                } else {
                    matchDistance += 10;
                }
            }

            if (converted)
                args[i+1] = v;
        }

        if (converted) {
            if ((scriptArgs.size() == (size_t)mtd.argumentCount())
                    && (matchDistance == 0)) {
                // perfect match, use this one
                chosenMethod = mtd;
                chosenIndex = index;
                break;
            } else {
                bool redundant = false;
                if ((callType != QMetaMethod::Constructor)
                        && (index < meta->methodOffset())) {
                    // it is possible that a virtual method is redeclared in a subclass,
                    // in which case we want to ignore the superclass declaration
                    for (int i = 0; i < candidates.size(); ++i) {
                        const QScriptMetaArguments &other = candidates.at(i);
                        if (mtd.types() == other.method.types()) {
                            redundant = true;
                            break;
                        }
                    }
                }
                if (!redundant) {
                    QScriptMetaArguments metaArgs(matchDistance, index, mtd, args);
                    if (candidates.isEmpty()) {
                        candidates.append(metaArgs);
                    } else {
                        const QScriptMetaArguments &otherArgs = candidates.at(0);
                        if ((args.count() > otherArgs.args.count())
                                || ((args.count() == otherArgs.args.count())
                                    && (matchDistance <= otherArgs.matchDistance))) {
                            candidates.prepend(metaArgs);
                        } else {
                            candidates.append(metaArgs);
                        }
                    }
                }
            }
        } else if (mtd.fullyResolved()) {
            conversionFailed.append(index);
        }

        if (!maybeOverloaded)
            break;
    }

    QLuaScriptValue result;

    if ((chosenIndex == -1) && candidates.isEmpty()) {
        QString funName = QString::fromLatin1(methodName(initialMethodSignature, nameLength));
        if (!conversionFailed.isEmpty()) {
            QString message = QString::fromLatin1("incompatible type of argument(s) in call to %0(); candidates were\n")
                    .arg(funName);
            for (int i = 0; i < conversionFailed.size(); ++i) {
                if (i > 0)
                    message += QLatin1String("\n");
                QMetaMethod mtd = metaMethod(meta, callType, conversionFailed.at(i));
                message += QString::fromLatin1("    %0").arg(QString::fromLatin1(mtd.signature()));
            }

            qWarning() << "[QtLua]" << message;

            result = QLuaScriptValue();
        } else if (!unresolved.isEmpty()) {
            QScriptMetaArguments argsInstance = unresolved.first();
            int unresolvedIndex = argsInstance.method.firstUnresolvedIndex();
            Q_ASSERT(unresolvedIndex != -1);
            QScriptMetaType unresolvedType = argsInstance.method.type(unresolvedIndex);
            QString unresolvedTypeName = QString::fromLatin1(unresolvedType.name());
            QString message = QString::fromLatin1("cannot call %0(): ")
                    .arg(funName);
            if (unresolvedIndex > 0) {
                message.append(QString::fromLatin1("argument %0 has unknown type `%1'").
                               arg(unresolvedIndex).arg(unresolvedTypeName));
            } else {
                message.append(QString::fromLatin1("unknown return type `%0'")
                               .arg(unresolvedTypeName));
            }
            message.append(QString::fromLatin1(" (register the type with qScriptRegisterMetaType())"));

            qWarning() << "[QtLua]" << message;

            result = QLuaScriptValue();
        } else {
            QString message = QString::fromLatin1("too few arguments in call to %0(); candidates are\n")
                    .arg(funName);
            for (int i = 0; i < tooFewArgs.size(); ++i) {
                if (i > 0)
                    message += QLatin1String("\n");
                QMetaMethod mtd = metaMethod(meta, callType, tooFewArgs.at(i));
                message += QString::fromLatin1("    %0").arg(QString::fromLatin1(mtd.signature()));
            }

            qWarning() << "[QtLua]" << message;

            result = QLuaScriptValue();
        }
    } else {
        if (chosenIndex == -1) {
            QScriptMetaArguments metaArgs = candidates.at(0);
            if ((candidates.size() > 1)
                    && (metaArgs.args.count() == candidates.at(1).args.count())
                    && (metaArgs.matchDistance == candidates.at(1).matchDistance)) {
                // ambiguous call
                QByteArray funName = methodName(initialMethodSignature, nameLength);
                QString message = QString::fromLatin1("ambiguous call of overloaded function %0(); candidates were\n")
                        .arg(QLatin1String(funName));
                for (int i = 0; i < candidates.size(); ++i) {
                    if (i > 0)
                        message += QLatin1String("\n");
                    QMetaMethod mtd = metaMethod(meta, callType, candidates.at(i).index);
                    message += QString::fromLatin1("    %0").arg(QString::fromLatin1(mtd.signature()));
                }

                qWarning() << "[QtLua]" << message;

                result = QLuaScriptValue();
            } else {
                chosenMethod = metaArgs.method;
                chosenIndex = metaArgs.index;
                args = metaArgs.args;
            }
        }

        if (chosenIndex != -1) {
            // call it
            //            context->calleeMetaIndex = chosenIndex;

#ifdef QT_DEBUG
//            qDebug() << "Final args are" << args;
#endif

            QVarLengthArray<void*, 9> array(args.count());
            void **params = array.data();
            for (int i = 0; i < args.count(); ++i) {
                const QVariant &v = args[i];
                switch (chosenMethod.type(i).kind()) {
                case QScriptMetaType::Variant:
                    params[i] = const_cast<QVariant*>(&v);
                    break;
                case QScriptMetaType::MetaType:
                case QScriptMetaType::MetaEnum:
                case QScriptMetaType::Unresolved:
                    params[i] = const_cast<void*>(v.constData());
                    break;
                default:
                    Q_ASSERT(0);
                }
            }

            {
                QLuaScriptContextCleanup context_cleanup(
                            exec,
                            exec->m_currentContext,
                            scriptArgs,
                            thisObject,
                            thisQObject);

                context_cleanup.overrideFunctionType(QLuaScriptContextInfo::QtFunction);

                if (callType == QMetaMethod::Constructor) {
                    Q_ASSERT(meta != 0);
                    meta->static_metacall(QMetaObject::CreateInstance, chosenIndex, params);
                } else {
                    QMetaObject::metacall(thisQObject, QMetaObject::InvokeMetaMethod, chosenIndex, params);
                }
            }

            QScriptMetaType retType = chosenMethod.returnType();
            if (retType.isVariant()) {
                result = exec->q_ptr->newFromVariant(*(QVariant *)params[0]);
            } else if (retType.typeId() != 0) {
                result = exec->q_ptr->newFromVariant(*(QVariant *)params[0]);
            } else {
                result = QLuaScriptValue(QLuaScriptValue::NilValue);
            }
        }
    }

    return result;
}

QLuaCachedMetaObject::QLuaCachedMetaObject()
    : m_listNode(this)
{
}

QLuaCachedMetaObject::~QLuaCachedMetaObject()
{
}

void QLuaCachedMetaObject::objectDeleted()
{
    m_qobjectWrapper = NULL;
}

QLuaMetaPropertyWrapper::QLuaMetaPropertyWrapper()
{
}

QLuaMetaPropertyWrapper::~QLuaMetaPropertyWrapper()
{
}

int QLuaMetaPropertyWrapper::dispatch_gc(lua_State * L)
{
    QPointer<QLuaScriptEngine> engine(QLuaEngineGlobals::engineFromLua(L));
    Q_ASSERT_X(engine != NULL, "QLuaMetaPropertyWrapper::dispatch_gc", "Unable to find Lua engine for lua_State");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);
    void * ptr = lua_touserdata(L, 1);
    int metaType = priv->m_ptrMetaTypes.value(ptr);
    Q_ASSERT_X(metaType == qMetaTypeId<QLuaInternal::QLuaMetaPropertyWrapper>(),
               "QLuaMetaPropertyWrapper::dispatch_gc",
               "Incorrect metatype detected when dispatching object pointer");
    QLuaMetaPropertyWrapper * self = reinterpret_cast<QLuaMetaPropertyWrapper *>(ptr);
    self->~QLuaMetaPropertyWrapper();
    if(!engine.isNull())
        priv->m_ptrMetaTypes.remove(ptr);
    else
        qWarning("[QtLua] QLuaMetaPropertyWrapper::dispatch_gc: Engine deleted in QObject destructor");
    return 0;
}

QLuaScriptValue QLuaMetaPropertyWrapper::create(QLuaQObjectWrapper * q_object,
                              int propertyIndex)
{
    QLuaScriptEngine * engine = q_object->m_engine;
    QLuaScriptValue obj_ref = engine->newUserData(sizeof(QLuaMetaPropertyWrapper));
    QLuaMetaPropertyWrapper * self = new(obj_ref.toUserData()) QLuaMetaPropertyWrapper();

    engine->registerUserData((void *)self, qMetaTypeId<QLuaInternal::QLuaMetaPropertyWrapper>());

    self->m_qobjectWrapper = q_object;
    self->m_propertyIndex = propertyIndex;

    obj_ref.setMetaTable(engine->getGlobal("QtLua").property(QLuaMetaPropertyWrapper_WrapperName));

    return obj_ref;
}

QLuaMetaMethodWrapper::QLuaMetaMethodWrapper()
{
}

QLuaMetaMethodWrapper::~QLuaMetaMethodWrapper()
{
}

int QLuaMetaMethodWrapper::dispatch_gc(lua_State * L)
{
    QPointer<QLuaScriptEngine> engine(QLuaEngineGlobals::engineFromLua(L));
    Q_ASSERT_X(engine != NULL, "QLuaMetaMethodWrapper::dispatch_gc", "Unable to find Lua engine for lua_State");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);
    void * ptr = lua_touserdata(L, 1);
    int metaType = priv->m_ptrMetaTypes.value(ptr);
    Q_ASSERT_X(metaType == qMetaTypeId<QLuaInternal::QLuaMetaMethodWrapper>(),
               "QLuaMetaPropertyWrapper::dispatch_gc",
               "Incorrect metatype detected when dispatching object pointer");
    QLuaMetaMethodWrapper * self = reinterpret_cast<QLuaMetaMethodWrapper *>(ptr);
    self->~QLuaMetaMethodWrapper();
    if(!engine.isNull())
        priv->m_ptrMetaTypes.remove(ptr);
    else
        qWarning("[QtLua] QLuaMetaMethodWrapper::dispatch_gc: Engine deleted in object destructor");
    return 0;
}

int QLuaMetaMethodWrapper::dispatch_call(lua_State * L)
{
    QPointer<QLuaScriptEngine> engine(QLuaEngineGlobals::engineFromLua(L));
    Q_ASSERT_X(engine != NULL, "QLuaMetaMethodWrapper::dispatch_gc", "Unable to find Lua engine for lua_State");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);

    int n_arg = lua_gettop(L);
    if(n_arg < 2) {
        QLuaException ex(engine->newException(QLatin1String("You need to call QObject function via : instead of .")));
        engine->throwException(ex);
    }

    if(!lua_isuserdata(L, 1))
    {
        QLuaException ex(engine->newException(QLatin1String("You need to call QObject function via : instead of . (invalid call argument types)")));
        engine->throwException(ex);
    }

    if(!lua_isuserdata(L, 2) && !lua_istable(L, 2))
    {
        QLuaException ex(engine->newException(QLatin1String("You need to call QObject function via : instead of . (invalid call argument types)")));
        engine->throwException(ex);
    }

    void * ptr1 = lua_touserdata(L, 1);

    if(priv->m_ptrMetaTypes.value(ptr1) != qMetaTypeId<QLuaInternal::QLuaMetaMethodWrapper>())
    {
        QLuaException ex(engine->newException(QLatin1String("You need to call QObject function via : instead of . (invalid call argument data ptrs)")));
        engine->throwException(ex);
    }

    QLuaScriptValue method_wrapper(QLuaScriptValue::newFromStack(engine, 1));
    QLuaScriptValue self_wrapper(QLuaScriptValue::newFromStack(engine, 2));

    QLuaMetaMethodWrapper * self_method = qtlua_scriptvalue_cast<QLuaMetaMethodWrapper>(method_wrapper);
    Q_ASSERT(self_method != NULL);

    QVector<QLuaScriptValue> wrap_args(n_arg != 2);
    for(int i = 0 ; i < n_arg - 2 ; ++i) {
        wrap_args[i] = QLuaScriptValue::newFromStack(engine, 3 + i);
    }

    QLuaScriptValue result = callQtMethod(priv,
                                          QMetaMethod::Slot,
                                          self_method->m_qobjectWrapper->m_objectPtr,
                                          wrap_args,
                                          self_wrapper,
                                          self_method->m_qobjectWrapper->m_metaObject,
                                          self_method->m_methodIndex,
                                          true);

    if(result.isValid()) {
        result.pushToStack(engine);
        return 1;
    }

    return 0;
}

QLuaScriptValue QLuaMetaMethodWrapper::create(QLuaQObjectWrapper * q_object,
                              int methodIndex)
{
    QLuaScriptEngine * engine = q_object->m_engine;
    QLuaScriptValue obj_ref = engine->newUserData(sizeof(QLuaMetaMethodWrapper));
    QLuaMetaMethodWrapper * self = new(obj_ref.toUserData()) QLuaMetaMethodWrapper();

    engine->registerUserData((void *)self, qMetaTypeId<QLuaInternal::QLuaMetaMethodWrapper>());

    self->m_qobjectWrapper = q_object;
    self->m_methodIndex = methodIndex;

    obj_ref.setMetaTable(engine->getGlobal("QtLua").property(QLuaMetaMethodWrapper_WrapperName));

    return obj_ref;
}

QLuaQObjectWrapper::QLuaQObjectWrapper()
{
#ifdef QT_DEBUG
    qDebug() << "[QtLua] Creating QObject wrapper at" << this;
#endif
}

QLuaQObjectWrapper::~QLuaQObjectWrapper()
{
#ifdef QT_DEBUG
    qDebug() << "[QtLua] Destroying QObject wrapper at" << this;
#endif

    for(QLinkedListNode * node = m_cachedItems.m_next ; node != &m_cachedItems ; node = node->m_next) {
        QLuaInternal::QLinkedListElement<QLuaCachedMetaObject> * p_node =
                static_cast<QLuaInternal::QLinkedListElement<QLuaCachedMetaObject> *>(node);
        p_node->m_ptr->objectDeleted();
    }

    if(m_objectPtr) {
        if(m_ownership == QLuaScriptEngine::ScriptOwnership) {
            delete m_objectPtr;
        } else if(m_ownership == QLuaScriptEngine::AutoOwnership) {
            if(!m_objectPtr->parent() || m_objectPtr->parent() == m_engine)
                delete m_objectPtr;
        }
    }
}

int QLuaQObjectWrapper::dispatch_gc(lua_State * L)
{
    QPointer<QLuaScriptEngine> engine(QLuaEngineGlobals::engineFromLua(L));
    Q_ASSERT_X(engine != NULL, "QLuaQObjectWrapper::dispatch_gc", "Unable to find Lua engine for lua_State");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);
    void * ptr = lua_touserdata(L, 1);
    int metaType = priv->m_ptrMetaTypes.value(ptr);
    Q_ASSERT_X(metaType == qMetaTypeId<QLuaInternal::QLuaQObjectWrapper>(),
               "QLuaQObjectWrapper::dispatch_gc",
               "Incorrect metatype detected when dispatching object pointer");
    QLuaQObjectWrapper * self = reinterpret_cast<QLuaQObjectWrapper *>(ptr);
    self->~QLuaQObjectWrapper();

    if(!engine.isNull())
        priv->m_ptrMetaTypes.remove(ptr);
    else
        qWarning("[QtLua] QLuaQObjectWrapper::dispatch_gc: Engine deleted in QObject destructor");

    return 0;
}

int QLuaQObjectWrapper::dispatch_index(lua_State * L)
{
    QPointer<QLuaScriptEngine> engine(QLuaEngineGlobals::engineFromLua(L));
    Q_ASSERT_X(engine != NULL, "QLuaQObjectWrapper::dispatch_gc", "Unable to find Lua engine for lua_State");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);
    void * wrapper_raw_ptr = lua_touserdata(L, lua_upvalueindex(1));
    Q_ASSERT_X(priv->m_ptrMetaTypes.value(wrapper_raw_ptr) == qMetaTypeId<QLuaInternal::QLuaQObjectWrapper>(),
               "QLuaQObjectWrapper::dispatch_index",
               "Wrapper upvalue pointer doesn't point to valid QLuaQObjectWrapper object");
    QLuaQObjectWrapper * wrapper_ptr = reinterpret_cast<QLuaQObjectWrapper *>(wrapper_raw_ptr);
    int n_args = lua_gettop(L);
    Q_ASSERT(n_args == 2);

    // 1 - self object
    // 2 - index

    QLuaScriptValue wrapper_object(QLuaScriptValue::newFromStack(engine, lua_upvalueindex(1)));
    QLuaScriptValue self_object(QLuaScriptValue::newFromStack(engine, 1));
    QLuaScriptValue index(QLuaScriptValue::newFromStack(engine, 2));

    QLuaScriptValue result(wrapper_ptr->handle_index(wrapper_object, self_object, index));
    if(result.isValid()) {
        result.pushToStack(engine);
        return 1;
    }

    return 0;
}

int QLuaQObjectWrapper::dispatch_newindex(lua_State * L)
{
    QPointer<QLuaScriptEngine> engine(QLuaEngineGlobals::engineFromLua(L));
    Q_ASSERT_X(engine != NULL, "QLuaQObjectWrapper::dispatch_gc", "Unable to find Lua engine for lua_State");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);
    void * wrapper_raw_ptr = lua_touserdata(L, lua_upvalueindex(1));
    Q_ASSERT_X(priv->m_ptrMetaTypes.value(wrapper_raw_ptr) == qMetaTypeId<QLuaInternal::QLuaQObjectWrapper>(),
               "QLuaQObjectWrapper::dispatch_index",
               "Wrapper upvalue pointer doesn't point to valid QLuaQObjectWrapper object");
    QLuaQObjectWrapper * wrapper_ptr = reinterpret_cast<QLuaQObjectWrapper *>(wrapper_raw_ptr);
    int n_args = lua_gettop(L);
    Q_ASSERT(n_args == 3);

    // 1 - self object
    // 2 - index
    // 3 - value

    QLuaScriptValue wrapper_object(QLuaScriptValue::newFromStack(engine, lua_upvalueindex(1)));
    QLuaScriptValue self_object(QLuaScriptValue::newFromStack(engine, 1));
    QLuaScriptValue index(QLuaScriptValue::newFromStack(engine, 2));
    QLuaScriptValue value(QLuaScriptValue::newFromStack(engine, 3));

    wrapper_ptr->handle_newindex(wrapper_object, self_object, index, value);

    return 0;
}

void QLuaQObjectWrapper::registerModule(QLuaScriptEngine * engine)
{
    QLuaScriptValue QLuaMetaPropertyWrapper_mt = engine->newTable();

    lua_State * L = QLuaScriptEnginePrivate::priv(engine)->L;

    lua_pushcclosure(L, QLuaMetaPropertyWrapper::dispatch_gc, 0);
    QLuaMetaPropertyWrapper_mt.setProperty("__gc", QLuaScriptValue::newFromStack(engine, -1));
    lua_pop(L, 1);

    engine->getGlobal("QtLua").setProperty(QLuaMetaPropertyWrapper_WrapperName, QLuaMetaPropertyWrapper_mt);

    QLuaScriptValue QLuaMetaMethodWrapper_mt = engine->newTable();

    lua_pushcclosure(L, QLuaMetaMethodWrapper::dispatch_gc, 0);
    QLuaMetaMethodWrapper_mt.setProperty("__gc", QLuaScriptValue::newFromStack(engine, -1));
    lua_pop(L, 1);

    lua_pushcclosure(L, QLuaMetaMethodWrapper::dispatch_call, 0);
    QLuaMetaMethodWrapper_mt.setProperty("__call", QLuaScriptValue::newFromStack(engine, -1));
    lua_pop(L, 1);

    engine->getGlobal("QtLua").setProperty(QLuaMetaMethodWrapper_WrapperName, QLuaMetaMethodWrapper_mt);
}

QLuaScriptValue QLuaQObjectWrapper::newQObject(QLuaScriptEngine * engine,
                                  QObject * q_object,
                                  QLuaScriptEngine::ValueOwnership ownership)
{
    Q_ASSERT_X(engine != NULL, "QLuaQObjectWrapper::newQObject", "Lua engine is NULL");
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(engine);
    lua_State * L = priv->L;

#ifdef QT_DEBUG
    qDebug() << "Wrapped QObject is" << q_object->metaObject()->className();
#endif

    // Create wrapper object
    QLuaQObjectWrapper * wrapper = new(lua_newuserdata(L, sizeof(QLuaQObjectWrapper))) QLuaQObjectWrapper();

    engine->registerUserData(wrapper, qMetaTypeId<QLuaInternal::QLuaQObjectWrapper>());

    wrapper->m_engine = engine;
    wrapper->m_objectPtr = q_object;
    wrapper->m_metaObject = q_object->metaObject();
    wrapper->m_ownership = ownership;

    lua_newtable(L);

    lua_pushcclosure(L, dispatch_gc, 0);
    lua_setfield(L, -2, "__gc");

    lua_pushvalue(L, -2);
    lua_pushcclosure(L, dispatch_index, 1);
    lua_setfield(L, -2, "__index");

    lua_pushvalue(L, -2);
    lua_pushcclosure(L, dispatch_newindex, 1);
    lua_setfield(L, -2, "__newindex");

    lua_newtable(L);
    lua_setfield(L, -2, "property_cache");

    lua_setmetatable(L, -2);

    QLuaScriptValue wrapper_ref(QLuaScriptValue::newFromStack(engine, -1));
    lua_pop(L, 1);

    return wrapper_ref;
}

QLuaScriptValue QLuaQObjectWrapper::handle_index(const QLuaScriptValue& wrapper_self,
                 const QLuaScriptValue& self,
                 const QLuaScriptValue& index)
{
    if(m_objectPtr.isNull()) {
        qWarning("[QtLua] QLuaQObjectWrapper::handle_index: QObject deleted");
        return QLuaScriptValue();
    }

//#ifdef QT_DEBUG
//    qDebug() << "Index" << m_objectPtr << "Wrapper self=" << wrapper_self <<
//                "Self =" << self << "index =" << index;
//#endif

    QLuaScriptValue property_cache(wrapper_self.metaTable().property("property_cache"));
    Q_ASSERT(property_cache.isValid());

    QByteArray index_str(index.toString());

    if(QLuaScriptClass * klass = qobject_cast<QLuaScriptClass *>(m_objectPtr.data()))
    {
        QLuaScriptClass::QueryFlags flags = klass->queryProperty(self, index_str);
        if(flags & QLuaScriptClass::HandlesCallAccess) {
            return klass->property(self, index_str);
        }
        if(flags & QLuaScriptClass::HandlesReadAccess) {
            return klass->property(self, index_str);
        }
    }

    QLuaScriptValue prop = property_cache.rawProperty(index_str);

    if(!prop.isNil()) {
        return prop;
    }

    const QMetaObject * mo = m_metaObject;

    int index_of_property = mo->indexOfProperty(index_str.constData());

    if(index_of_property != -1)
    {
        // Cache on structure-like properties as they don't need the access.
        QMetaProperty mp(mo->property(index_of_property));

        if(!mp.isReadable()) {
            qWarning() << "[QtLua] Trying to read non-readable property" << mp.name();
            return QLuaScriptValue();
        }

        QVector<QLuaScriptValue> args;
        args << self << index;

        QLuaScriptContextCleanup context_cleanup(QLuaScriptEnginePrivate::priv(m_engine),
                                                 QLuaScriptEnginePrivate::priv(m_engine)->m_currentContext,
                                                 args,
                                                 self,
                                                 this->m_objectPtr);

        context_cleanup.overrideFunctionType(QLuaScriptContextInfo::QtPropertyFunction);

        QVariant result = mp.read(m_objectPtr);

        if(!m_objectPtr) {
            qWarning() << "[QtLua] QObject deleted while reading property" << index_str;
            return QLuaScriptValue();
        }

        if(!m_engine) {
            qWarning() << "[QtLua] Script engine deleted while reading property" << index_str;
            return QLuaScriptValue();
        }

        return m_engine->newFromVariant(result);
    }

    int method_count = mo->methodCount();
    for(int i = 0 ; i < method_count ; ++i)
    {
        QMetaMethod mm(mo->method(i));
        if(methodNameEquals(mm, index_str.constData(), index_str.length())) {
            // Found method by name

            QLuaScriptValue wrapper(QLuaMetaMethodWrapper::create(this, i));
            property_cache.setRawProperty(index_str, wrapper);
            return wrapper;
        }
    }

    return QLuaScriptValue();
}

void QLuaQObjectWrapper::handle_newindex(const QLuaScriptValue& wrapper_self,
                     const QLuaScriptValue& self,
                     const QLuaScriptValue& index,
                     const QLuaScriptValue& value)
{
    QLuaScriptEnginePrivate * priv = QLuaScriptEnginePrivate::priv(m_engine);

    if(m_objectPtr.isNull()) {
        qWarning("[QtLua] QLuaQObjectWrapper::handle_newindex: QObject deleted");
        return;
    }

    QLuaScriptValue property_cache(wrapper_self.metaTable().property("property_cache"));
    Q_ASSERT(property_cache.isValid());

    QByteArray index_str(index.toString());

    if(QLuaScriptClass * klass = qobject_cast<QLuaScriptClass *>(m_objectPtr.data()))
    {
        QLuaScriptClass::QueryFlags flags = klass->queryProperty(self, index_str);
        if(flags & QLuaScriptClass::HandlesCallAccess) {
            klass->setProperty(self, index_str, value);
            return;
        }
    }

    const QMetaObject * mo = m_metaObject;

    int index_of_property = mo->indexOfProperty(index_str.constData());

    if(index_of_property != -1)
    {
        // Cache on structure-like properties as they don't need the access.
        QMetaProperty mp(mo->property(index_of_property));

        if(!mp.isWritable()) {
            qWarning() << "[QtLua] Trying to write read-only property" << mp.name();
            return;
        }

        QVector<QLuaScriptValue> args;
        args << self << index << value;

        QLuaScriptContextCleanup context_cleanup(QLuaScriptEnginePrivate::priv(m_engine),
                                                 QLuaScriptEnginePrivate::priv(m_engine)->m_currentContext,
                                                 args,
                                                 self,
                                                 this->m_objectPtr);

        context_cleanup.overrideFunctionType(QLuaScriptContextInfo::QtPropertyFunction);

        QVariant variant;

        if(!priv->convertScriptValueToPropertyType(mp.isFlagType() || mp.isEnumType(),
                                                   mp.userType(),
                                                   value,
                                                   variant))
        {
            qWarning() << "[QtLua] Failed to find type converter for property" << mp.name();
            return;
        }

        if(!mp.write(m_objectPtr, variant))
        {
            qWarning() << "[QtLua] Write to property failed";
            return;
        }

        return;
    }
}

}
